package com.elitesland.org.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.elitesland.core.base.BaseModel;
import com.elitesland.core.base.PagingVO;
import com.elitesland.core.exception.BusinessException;
import com.elitesland.org.convert.OrgBuConvert;
import com.elitesland.org.entity.OrgBuDO;
import com.elitesland.org.entity.OrgBuEmployeeDO;
import com.elitesland.org.entity.QOrgBuDO;
import com.elitesland.org.param.OrgBuQParam;
import com.elitesland.org.repo.OrgBuRepo;
import com.elitesland.org.service.OrgBuEmployeeService;
import com.elitesland.org.service.OrgBuService;
import com.elitesland.org.service.OrgOuService;
import com.elitesland.org.vo.OrgBuVO;
import com.elitesland.org.vo.OrgOuVO;
import com.elitesland.system.annotation.SysCodeProc;
import com.querydsl.core.types.ExpressionUtils;
import com.querydsl.core.types.Predicate;
import lombok.val;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

@Service
public class OrgBuServiceImpl implements OrgBuService {

    private static final List<String> virtualBuTypes = Arrays.asList(
            "COST_CENTER",
            "REVENUE_CENTER",
            "EXPENSE_CENTER",
            "INVESTMENT_CENTER",
            "PROFIT_CENTER");

    private OrgBuRepo orgBuRepo;

    private OrgBuEmployeeService orgBuEmployeeService;

    private OrgOuService orgOuService;

    @Autowired
    public void setOrgBuRepo(OrgBuRepo orgBuRepo) {
        this.orgBuRepo = orgBuRepo;
    }

    @Autowired
    public void setOrgBuEmployeeService(OrgBuEmployeeService orgBuEmployeeService) {
        this.orgBuEmployeeService = orgBuEmployeeService;
    }

    @Autowired
    public void setOrgOuService(OrgOuService orgOuService) {
        this.orgOuService = orgOuService;
    }

    private final QOrgBuDO bus = QOrgBuDO.orgBuDO;

    @Override
    public PagingVO<OrgBuVO> searchBus(Long ouId, OrgBuQParam param) {

        Predicate predicate = bus.ouId.eq(ouId);

        predicate = StrUtil.isBlank(param.getCode()) ? predicate :
                ExpressionUtils.and(predicate, bus.code.like("%" + param.getCode() + "%"));
        predicate = StrUtil.isBlank(param.getName()) ? predicate :
                ExpressionUtils.and(predicate, bus.name.like("%" + param.getName() + "%"));
        predicate = StrUtil.isBlank(param.getAbbr()) ? predicate :
                ExpressionUtils.and(predicate, bus.abbr.like("%" + param.getAbbr() + "%"));
        predicate = StrUtil.isBlank(param.getType()) ? predicate :
                ExpressionUtils.and(predicate, bus.type.eq(param.getType()));
        predicate = StrUtil.isBlank(param.getType2()) ? predicate :
                ExpressionUtils.and(predicate, bus.type2.eq(param.getType2()));
        predicate = StrUtil.isBlank(param.getType3()) ? predicate :
                ExpressionUtils.and(predicate, bus.type3.eq(param.getType3()));
        predicate = StrUtil.isBlank(param.getType4()) ? predicate :
                ExpressionUtils.and(predicate, bus.type4.eq(param.getType4()));
        predicate = StrUtil.isBlank(param.getType5()) ? predicate :
                ExpressionUtils.and(predicate, bus.type5.eq(param.getType5()));
        predicate = StrUtil.isBlank(param.getType6()) ? predicate :
                ExpressionUtils.and(predicate, bus.type6.eq(param.getType6()));
        predicate = StrUtil.isBlank(param.getType7()) ? predicate :
                ExpressionUtils.and(predicate, bus.type7.eq(param.getType7()));
        predicate = StrUtil.isBlank(param.getStatus()) ? predicate :
                ExpressionUtils.and(predicate, bus.status.eq(param.getStatus()));
        predicate = StrUtil.isBlank(param.getStatus2()) ? predicate :
                ExpressionUtils.and(predicate, bus.status2.eq(param.getStatus2()));
        predicate = StrUtil.isBlank(param.getStatus3()) ? predicate :
                ExpressionUtils.and(predicate, bus.status3.eq(param.getStatus3()));
        predicate = param.getParentId() == null || param.getParentId() <= 0 ? predicate :
                ExpressionUtils.and(predicate, bus.parentId.eq(param.getParentId()));
        predicate = param.getAddrNo() == null ? predicate :
                ExpressionUtils.and(predicate, bus.addrNo.eq(param.getAddrNo()));
        predicate = StrUtil.isBlank(param.getLevel()) ? predicate :
                ExpressionUtils.and(predicate, bus.level.eq(param.getLevel()));
        predicate = StrUtil.isBlank(param.getRegion()) ? predicate :
                ExpressionUtils.and(predicate, bus.region.eq(param.getRegion()));
        predicate = param.getChargeEmpId() == null ? predicate :
                ExpressionUtils.and(predicate, bus.chargeEmpId.eq(param.getChargeEmpId()));
        predicate = param.getRefBuId() == null || param.getRefBuId() <= 0 ? predicate :
                ExpressionUtils.and(predicate, bus.refBuId.eq(param.getRefBuId()));
        predicate = param.getRefBuId2() == null || param.getRefBuId2() <= 0 ? predicate :
                ExpressionUtils.and(predicate, bus.refBuId2.eq(param.getRefBuId2()));
        predicate = param.getRefBuId3() == null || param.getRefBuId3() <= 0 ? predicate :
                ExpressionUtils.and(predicate, bus.refBuId3.eq(param.getRefBuId3()));
        predicate = param.getRefBuId4() == null || param.getRefBuId4() <= 0 ? predicate :
                ExpressionUtils.and(predicate, bus.refBuId4.eq(param.getRefBuId4()));
        predicate = param.getRefBuId5() == null || param.getRefBuId5() <= 0 ? predicate :
                ExpressionUtils.and(predicate, bus.refBuId5.eq(param.getRefBuId5()));
        predicate = param.getRefBuId6() == null || param.getRefBuId6() <= 0 ? predicate :
                ExpressionUtils.and(predicate, bus.refBuId6.eq(param.getRefBuId6()));
        predicate = StrUtil.isBlank(param.getExternalCode()) ? predicate :
                ExpressionUtils.and(predicate, bus.externalCode.like("%" + param.getExternalCode() + "%"));

        val ret = orgBuRepo.findAll(predicate, param.getPageRequest());
        return PagingVO.<OrgBuVO>builder()
                .total(ret.getTotalElements())
                .records(ret.stream().map(OrgBuConvert.INSTANCE::doToVO)
                        .collect(Collectors.toList())).build();
    }

    @Override
    @SysCodeProc
    public List<OrgBuVO> searchBuByTree(Long ouId, OrgBuQParam param) {

        Predicate predicate = bus.ouId.eq(ouId);

        if (StrUtil.isNotBlank(param.getKeyword())) {
            val cond = "%" + param.getKeyword() + "%";

            predicate = ExpressionUtils.and(predicate, bus.code.like(cond)
                    .or(bus.name.like(cond))
                    .or(bus.abbr.like(cond)));
        }

        predicate = param.getIsVirtual() == null ? predicate :
                ExpressionUtils.and(predicate, bus.isVirtual.eq(param.getIsVirtual()));
        predicate = StrUtil.isBlank(param.getType()) ? predicate :
                ExpressionUtils.and(predicate, bus.type.eq(param.getType()));

        val targetBus = orgBuRepo.findAll(predicate);
        val targetIds = StreamSupport.stream(targetBus.spliterator(), false).map(BaseModel::getId)
                .collect(Collectors.toList());
        val allIds = StreamSupport.stream(targetBus.spliterator(), false).map(b -> {
            return Arrays.stream(b.getPath().split("/"))
                    .filter(StrUtil::isNotBlank)
                    .map(Long::valueOf)
                    .collect(Collectors.toList());
        }).reduce(new ArrayList<>(), (all, item) -> {
            if (CollUtil.isNotEmpty(item)) {
                all.removeAll(item);
                all.addAll(item);
            }
            return all;
        }).stream().distinct().collect(Collectors.toList());

        allIds.removeAll(targetIds);
        allIds.addAll(targetIds);

        val buList = orgBuRepo.findAllByOuIdAndIdIn(ouId, allIds)
                .stream().map(OrgBuConvert.INSTANCE::doToVO).collect(Collectors.toList());

        val ouIds = buList.stream().map(OrgBuVO::getOuId).collect(Collectors.toList());
        val ous = orgOuService.listByIds(ouIds);

        buList.forEach(bu -> {
            for (OrgOuVO ouVO : ous) {
                if (bu.getOuId().equals(ouVO.getId())) {
                    bu.setOu(ouVO);
                    break;
                }
            }
        });

        return buildTree(0L, buList);
    }

    @Override
    public List<OrgBuVO> listAllById(Long buId) {
        val buOpt = oneById(buId);
        if (buOpt.isPresent()) {
            val allBus = orgBuRepo.findAllByPathLike(buOpt.get().getPath() + "%");
            return allBus.stream().map(OrgBuConvert.INSTANCE::doToVO)
                    .collect(Collectors.toList());
        } else {
            return Collections.emptyList();
        }
    }

    private List<OrgBuVO> buildTree(Long parentId, List<OrgBuVO> buList) {
        val treeList = new ArrayList<OrgBuVO>();

        buList.stream().filter(b -> b.getParentId().equals(parentId)).forEach(b -> {
            b.setChildren(buildTree(b.getId(), buList));

            treeList.add(b);
        });

        return treeList;
    }

    @Override
    public Optional<OrgBuVO> oneById(Long id) {
        return orgBuRepo.findById(id).map(OrgBuConvert.INSTANCE::doToVO)
                .map(bu -> {
                    val parentBu = oneById(bu.getParentId());
                    bu.setParentBu(parentBu.orElse(null));

                    val ou = orgOuService.oneById(bu.getOuId());
                    bu.setOu(ou.orElse(null));

                    if (bu.getRefBuId() != null && bu.getRefBuId() > 0) {
                        val b = oneById(bu.getRefBuId());
                        bu.setRefBu(b.orElse(null));
                    }
                    if (bu.getRefBuId() != null && bu.getRefBuId() > 0) {
                        val b = oneById(bu.getRefBuId());
                        bu.setRefBu(b.orElse(null));
                    }
                    if (bu.getRefBuId2() != null && bu.getRefBuId2() > 0) {
                        val b = oneById(bu.getRefBuId2());
                        bu.setRefBu2(b.orElse(null));
                    }
                    if (bu.getRefBuId3() != null && bu.getRefBuId3() > 0) {
                        val b = oneById(bu.getRefBuId3());
                        bu.setRefBu3(b.orElse(null));
                    }
                    if (bu.getRefBuId4() != null && bu.getRefBuId4() > 0) {
                        val b = oneById(bu.getRefBuId4());
                        bu.setRefBu4(b.orElse(null));
                    }
                    if (bu.getRefBuId5() != null && bu.getRefBuId5() > 0) {
                        val b = oneById(bu.getRefBuId5());
                        bu.setRefBu5(b.orElse(null));
                    }

                    return bu;
                });
    }

    @Override
    @Transactional(rollbackOn = {Exception.class})
    public Long create(OrgBuVO bu) {
        val saveBu = OrgBuConvert.INSTANCE.voToDO(bu);

        if (StrUtil.isNotBlank(saveBu.getType())) {
            saveBu.setIsVirtual(virtualBuTypes.contains(saveBu.getType()));
        }

        saveBu.setPath("/");
        oneById(bu.getParentId()).ifPresent(pb -> {
            saveBu.setPath(pb.getPath() + pb.getId() + "/");
        });

        val oldBus = orgBuRepo.findAllByParentIdOrderBySortNo(saveBu.getParentId());

        var index = 1;
        for (OrgBuDO b : oldBus) {
            b.setSortNo(index);
            index++;
        }

        orgBuRepo.saveAll(oldBus);

        saveBu.setSortNo(index);

        val newBu = orgBuRepo.save(saveBu);

        return newBu.getId();
    }

    @Override
    public void update(OrgBuVO bu) {
        orgBuRepo.findById(bu.getId()).ifPresent(oldBu -> {
            oldBu.setName(bu.getName());

            orgBuRepo.save(oldBu);
        });
    }

    @Override
    public void removeById(Long id) {
        oneById(id).ifPresent(b -> {
            orgBuRepo.deleteAllByPathLike(b.getPath() + b.getId() + "/%");
        });
    }

    @Override
    public void addEmployeeLinks(Long buId, List<Long> employeeIds, String linkType) {
        orgBuRepo.findById(buId).ifPresent(b -> {
            if (b.getIsVirtual()) {
                throw new BusinessException("雇员不能和财务中心关联");
            }

            val links = employeeIds.stream().map(id -> {
                return new OrgBuEmployeeDO()
                        .setBuId(buId)
                        .setEmployeeId(id)
                        .setLinkType(linkType);
            }).collect(Collectors.toList());

            orgBuEmployeeService.linkEmployeesToBu(buId, links);
        });
    }

    @Override
    @Transactional(rollbackOn = {Exception.class})
    public void bindBusToBu(Long buId, List<Long> buIds) {
        oneById(buId).ifPresent(bu -> {
            val isVirtual = virtualBuTypes.contains(bu.getType());

            val subBus = orgBuRepo.findAllByIdIn(buIds);
            subBus.forEach(subBu -> {
                if (isVirtual) {
                    if (!subBu.getType().equals(bu.getType())) {
                        throw new BusinessException("核算中心的下级部门的类型必须和上级的一致");
                    }
                } else {
                    if (virtualBuTypes.contains(subBu.getType())) {
                        throw new BusinessException("不能把核算中心类部门关联到实体BU下");
                    }
                }
                subBu.setParentId(bu.getParentId())
                        .setPath(bu.getPath() + bu.getId() + "/");
            });
            orgBuRepo.saveAll(subBus);
        });
    }
}
